/**
 * @class Ext.state.Provider
 * <p>Abstract base class for state provider implementations. The provider is responsible
 * for setting values  and extracting values to/from the underlying storage source. The 
 * storage source can vary and the details should be implemented in a subclass. For example
 * a provider could use a server side database or the browser localstorage where supported.</p>
 *
 * <p>This class provides methods for encoding and decoding <b>typed</b> variables including 
 * dates and defines the Provider interface. By default these methods put the value and the
 * type information into a delimited string that can be stored. These should be overridden in 
 * a subclass if you want to change the format of the encoded value and subsequent decoding.</p>
 */
Ext.define('Ext.state.Provider', {
    mixins: {
        observable: 'Ext.util.Observable'
    },

    /**
     * @cfg {String} prefix A string to prefix to items stored in the underlying state store. 
     * Defaults to <tt>'ext-'</tt>
     */
    prefix: 'ext-',

    /**
     * @event statechange
     * Fires when a state change occurs.
     * @param {Ext.state.Provider} this This state provider
     * @param {String} key The state key which was changed
     * @param {String} value The encoded value for the state
     */

    constructor: function(config) {
        var me = this;

        Ext.apply(me, config);
        me.state = {};
        me.mixins.observable.constructor.call(me);
    },

    /**
     * Returns the current value for a key
     * @param {String} name The key name
     * @param {Object} defaultValue A default value to return if the key's value is not found
     * @return {Object} The state data
     */
    get: function(name, defaultValue) {
        var ret = this.state[name];

        return ret === undefined ? defaultValue : ret;
    },

    /**
     * Clears a value from the state
     * @param {String} name The key name
     */
    clear: function(name) {
        var me = this;

        delete me.state[name];
        me.fireEvent("statechange", me, name, null);
    },

    /**
     * Sets the value for a key
     * @param {String} name The key name
     * @param {Object} value The value to set
     */
    set: function(name, value) {
        var me = this;

        me.state[name] = value;
        me.fireEvent("statechange", me, name, value);
    },

    /**
     * Decodes a string previously encoded with {@link #encodeValue}.
     * @param {String} value The value to decode
     * @return {Object} The decoded value
     */
    decodeValue: function(value) {
        // a -> Array
        // n -> Number
        // d -> Date
        // b -> Boolean
        // s -> String
        // o -> Object
        // -> Empty (null)

        var me = this,
            re = /^(a|n|d|b|s|o|e):(.*)$/,
            matches = re.exec(unescape(value)),
            all, type, keyValue, values, vLen, v;

        if (!matches || !matches[1]) {
            return; // non state
        }

        type = matches[1];
        value = matches[2];

        switch (type) {
            case 'e':
                return null;

            case 'n':
                return parseFloat(value);

            case 'd':
                return new Date(Date.parse(value));

            case 'b':
                return (value === '1');

            case 'a':
                all = [];

                if (value) {
                    values = value.split('^');
                    vLen = values.length;

                    for (v = 0; v < vLen; v++) {
                        value = values[v];
                        all.push(me.decodeValue(value));
                    }
                }

                return all;

            case 'o':
                all = {};

                if (value) {
                    values = value.split('^');
                    vLen = values.length;

                    for (v = 0; v < vLen; v++) {
                        value = values[v];
                        keyValue = value.split('=');
                        all[keyValue[0]] = me.decodeValue(keyValue[1]);
                    }
                }

                return all;

            default:
                return value;
        }
    },

    /**
     * Encodes a value including type information.  Decode with {@link #decodeValue}.
     * @param {Object} value The value to encode
     * @return {String} The encoded value
     */
    encodeValue: function(value) {
        var flat = '',
            i = 0,
            enc, len, key;

        if (value == null) {
            return 'e:1';
        }
        else if (typeof value === 'number') {
            enc = 'n:' + value;
        }
        else if (typeof value === 'boolean') {
            enc = 'b:' + (value ? '1' : '0');
        }
        else if (Ext.isDate(value)) {
            enc = 'd:' + value.toUTCString();
        }
        else if (Ext.isArray(value)) {
            for (len = value.length; i < len; i++) {
                flat += this.encodeValue(value[i]);

                if (i !== len - 1) {
                    flat += '^';
                }
            }

            enc = 'a:' + flat;
        }
        else if (typeof value === 'object') {
            for (key in value) {
                if (typeof value[key] !== 'function' && value[key] !== undefined) {
                    flat += key + '=' + this.encodeValue(value[key]) + '^';
                }
            }

            enc = 'o:' + flat.substring(0, flat.length - 1);
        }
        else {
            enc = 's:' + value;
        }

        return escape(enc);
    }
});
